/*
 +---------------------------------------------------------------------------+
 | Facebook Development Platform Java Client                                 |
 +---------------------------------------------------------------------------+
 | Copyright (c) 2007-2008 Facebook, Inc.                                    |
 | All rights reserved.                                                      |
 |                                                                           |
 | Redistribution and use in source and binary forms, with or without        |
 | modification, are permitted provided that the following conditions        |
 | are met:                                                                  |
 |                                                                           |
 | 1. Redistributions of source code must retain the above copyright         |
 |    notice, this list of conditions and the following disclaimer.          |
 | 2. Redistributions in binary form must reproduce the above copyright      |
 |    notice, this list of conditions and the following disclaimer in the    |
 |    documentation and/or other materials provided with the distribution.   |
 |                                                                           |
 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
 +---------------------------------------------------------------------------+
 | For help with this library, contact developers-help@facebook.com          |
 +---------------------------------------------------------------------------+
 */
package com.facebook.api;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class FacebookSignatureUtil {
    private FacebookSignatureUtil() {
    }

    /**
     * Out of the passed in <code>reqParams</code>, extracts the parameters that are in the
     * FacebookParam namespace and returns them.
     * 
     * @param reqParams A map of request parameters to their values. Values are arrays of strings,
     *            as returned by ServletRequest.getParameterMap(). Only the first element in a given
     *            array is significant.
     * @return a boolean indicating whether the calculated signature matched the expected signature
     */
    public static Map<String,CharSequence> extractFacebookParamsFromArray(
            Map<CharSequence,CharSequence[]> reqParams) {
        if (null == reqParams)
            return null;
        Map<String,CharSequence> result = new HashMap<String,CharSequence>(reqParams.size());
        for (Map.Entry<CharSequence,CharSequence[]> entry: reqParams.entrySet()) {
            String key = entry.getKey().toString();
            if (FacebookParam.isInNamespace(key)) {
                CharSequence[] value = entry.getValue();
                if (value.length > 0)
                    result.put(key, value[0]);
            }
        }
        return result;
    }

    /**
     * Out of the passed in <code>reqParams</code>, extracts the parameters that are in the
     * FacebookParam namespace and returns them.
     * 
     * @param reqParams a map of request parameters to their values
     * @return a boolean indicating whether the calculated signature matched the expected signature
     */
    public static Map<String,CharSequence> extractFacebookNamespaceParams(
            Map<CharSequence,CharSequence> reqParams) {
        if (null == reqParams)
            return null;
        Map<String,CharSequence> result = new HashMap<String,CharSequence>(reqParams.size());
        for (Map.Entry<CharSequence,CharSequence> entry: reqParams.entrySet()) {
            String key = entry.getKey().toString();
            if (FacebookParam.isInNamespace(key))
                result.put(key, entry.getValue());
        }
        return result;
    }

    /**
     * Out of the passed in <code>reqParams</code>, extracts the parameters that are known
     * FacebookParams and returns them.
     * 
     * @param reqParams a map of request parameters to their values
     * @return a map suitable for being passed to verify signature
     */
    public static EnumMap<FacebookParam,CharSequence> extractFacebookParams(
            Map<CharSequence,CharSequence> reqParams) {
        if (null == reqParams)
            return null;
        EnumMap<FacebookParam,CharSequence> result = new EnumMap<FacebookParam,CharSequence>(
                FacebookParam.class);
        for (Map.Entry<CharSequence,CharSequence> entry: reqParams.entrySet()) {
            FacebookParam matchingFacebookParam = FacebookParam.get(entry.getKey().toString());
            if (null != matchingFacebookParam) {
                result.put(matchingFacebookParam, entry.getValue());
            }
        }
        return result;
    }

    /**
     * Verifies that a signature received matches the expected value. Removes
     * FacebookParam.SIGNATURE from params if present.
     * 
     * @param params a map of parameters and their values, such as one obtained from
     *            extractFacebookParams; expected to the expected signature as the
     *            FacebookParam.SIGNATURE parameter
     * @param secret
     * @return a boolean indicating whether the calculated signature matched the expected signature
     */
    public static boolean verifySignature(EnumMap<FacebookParam,CharSequence> params, String secret) {
        if (null == params || params.isEmpty())
            return false;
        CharSequence sigParam = params.remove(FacebookParam.SIGNATURE);
        return (null == sigParam)? false: verifySignature(params, secret, sigParam.toString());
    }

    /**
     * Verifies that a signature received matches the expected value.
     * 
     * @param params a map of parameters and their values, such as one obtained from
     *            extractFacebookParams
     * @return a boolean indicating whether the calculated signature matched the expected signature
     */
    public static boolean verifySignature(EnumMap<FacebookParam,CharSequence> params,
            String secret, String expected) {
        assert !(null == secret || "".equals(secret));
        if (null == params || params.isEmpty())
            return false;
        if (null == expected || "".equals(expected)) {
            return false;
        }
        params.remove(FacebookParam.SIGNATURE);
        List<String> sigParams = convertFacebookParams(params.entrySet());
        return verifySignature(sigParams, secret, expected);
    }

    /**
     * Verifies that a signature received matches the expected value. Removes
     * FacebookParam.SIGNATURE from params if present.
     * 
     * @param params a map of parameters and their values, such as one obtained from
     *            extractFacebookNamespaceParams; expected to contain the signature as the
     *            FacebookParam.SIGNATURE parameter
     * @param secret
     * @return a boolean indicating whether the calculated signature matched the expected signature
     */
    public static boolean verifySignature(Map<String,CharSequence> params, String secret) {
        if (null == params || params.isEmpty())
            return false;
        CharSequence sigParam = params.remove(FacebookParam.SIGNATURE.toString());
        return (null == sigParam)? false: verifySignature(params, secret, sigParam.toString());
    }

    /**
     * Verifies that a signature received matches the expected value.
     * 
     * @param params a map of parameters and their values, such as one obtained from
     *            extractFacebookNamespaceParams
     * @return a boolean indicating whether the calculated signature matched the expected signature
     */
    public static boolean verifySignature(Map<String,CharSequence> params, String secret,
            String expected) {
        assert !(null == secret || "".equals(secret));
        if (null == params || params.isEmpty())
            return false;
        if (null == expected || "".equals(expected)) {
            return false;
        }
        params.remove(FacebookParam.SIGNATURE.toString());
        List<String> sigParams = convert(params.entrySet());
        return verifySignature(sigParams, secret, expected);
    }

    private static boolean verifySignature(List<String> sigParams, String secret, String expected) {
        if (null == expected || "".equals(expected))
            return false;
        String signature = generateSignature(sigParams, secret);
        return expected.equals(signature);
    }

    /**
     * Converts a Map of key-value pairs into the form expected by generateSignature
     * 
     * @param entries a collection of Map.Entry's, such as can be obtained using myMap.entrySet()
     * @return a List suitable for being passed to generateSignature
     */
    public static List<String> convert(Collection<Map.Entry<String,CharSequence>> entries) {
        List<String> result = new ArrayList<String>(entries.size());
        for (Map.Entry<String,CharSequence> entry: entries)
            result.add(FacebookParam.stripSignaturePrefix(entry.getKey()) + "=" + entry.getValue());
        return result;
    }

    /**
     * Converts a Map of key-value pairs into the form expected by generateSignature
     * 
     * @param entries a collection of Map.Entry's, such as can be obtained using myMap.entrySet()
     * @return a List suitable for being passed to generateSignature
     */
    public static List<String> convertFacebookParams(
            Collection<Map.Entry<FacebookParam,CharSequence>> entries) {
        List<String> result = new ArrayList<String>(entries.size());
        for (Map.Entry<FacebookParam,CharSequence> entry: entries)
            result.add(entry.getKey().getSignatureName() + "=" + entry.getValue());
        return result;
    }

    /**
     * Calculates the signature for the given set of params using the supplied secret
     * 
     * @param params Strings of the form "key=value"
     * @param secret
     * @return the signature
     */
    public static String generateSignature(List<String> params, String secret) {
        StringBuffer buffer = new StringBuffer();
        Collections.sort(params);
        for (String param: params) {
            buffer.append(param);
        }
        buffer.append(secret);
        try {
            java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
            StringBuffer result = new StringBuffer();
            for (byte b: md.digest(buffer.toString().getBytes())) {
                result.append(Integer.toHexString((b & 0xf0) >>> 4));
                result.append(Integer.toHexString(b & 0x0f));
            }
            return result.toString();
        } catch (java.security.NoSuchAlgorithmException ex) {
            System.err.println("MD5 does not appear to be supported" + ex);
            return "";
        }
    }
}
